local sdbus = require 'sd_bus'
local Class = require 'mc.class'
local AppBase = require 'mc.app_base'
local org_freedesktop_dbus = require 'sd_bus.org_freedesktop_dbus'
local skynet = require 'skynet'
local log = require 'mc.logging'
local error_common = require 'mc.error.common'
local safe_call = error_common.safe_call
local PING_TIMEOUT_MS<const> = 5000

local function ping(bus, times, sleep_time, service_name, app_path)
    times = times or 20
    sleep_time = sleep_time or 50
    local err
    for i = 1, times do
        err = org_freedesktop_dbus.timeout_ping(bus, PING_TIMEOUT_MS, service_name, app_path)
        if not err then
            return
        end
        if i < times then
            log:warn('ping %s %s failed %s time%s, err: %s, retrying...', service_name, app_path, i,
                i == 1 and '' or 's', err)
        end
        skynet.sleep(sleep_time)
    end
    log:error('ping %s %s failed, err: %s', service_name, app_path, err)
    error(string.format('ping %s %s failed', service_name, app_path))
end

local BasicClient = Class(AppBase)

function BasicClient:call(bus_or_app, method, signature, ...)
    local bus = (getmetatable(bus_or_app) == sdbus.SDBus) and bus_or_app or bus_or_app.bus
    return bus:call(self.service_name, self.app_path, self.app_interface, method, signature, ...)
end

function BasicClient:__call(Rsp, ...)
    if not Rsp then
        return self:call(...)
    end

    return Rsp.new(self:call(...))
end

function BasicClient:pcall(bus_or_app, method, signature, ...)
    local bus = (getmetatable(bus_or_app) == sdbus.SDBus) and bus_or_app or bus_or_app.bus
    return bus:pcall(self.service_name, self.app_path, self.app_interface, method, signature, ...)
end

local function pcall_result(Rsp, err, ...)
    if err then
        return err, nil
    elseif Rsp == nil then
        return
    end
    return nil, Rsp.new(...)
end

function BasicClient:__pcall(Rsp, ...)
    return pcall_result(Rsp, self:pcall(...))
end

function BasicClient:ping(bus, times, sleep_time)
    if self.__call == self.__pcall then
        return safe_call(ping, bus, times, sleep_time, self.service_name, self.app_path)
    end
    ping(bus, times, sleep_time, self.service_name, self.app_path)
end

function BasicClient:close()
end

local Client = Class(AppBase)

function Client:init()
    if type(self.signal_slots) ~= 'table' then
        return
    end
    self.__signal_slots = {}
    local mt = getmetatable(self.signal_slots) or {}
    mt.__index = function(_, index)
        return self.__signal_slots[index]
    end
    mt.__newindex = function(_, index, value)
        if value == nil or self.__signal_slots[index] == nil then
            self.__signal_slots[index] = value
            return
        end
        table.insert(self.__signal_slots, value)
    end
    mt.__len = function(_)
        return #self.__signal_slots
    end
    mt.__pairs = function(_)
        return pairs(self.__signal_slots)
    end
    mt.__ipairs = function(_)
        return ipairs(self.__signal_slots)
    end
    setmetatable(self.signal_slots, mt)
end

function Client:get_bus()
    if self.bus then
        return self.bus
    end
    self.bus = sdbus.open_user(true)
    self.bus.service_name = self.service_name
    return self.bus
end

function Client:call(method, signature, ...)
    return self:get_bus():call(self.service_name, self.app_path, self.app_interface, method, signature, ...)
end

function Client:pcall(method, signature, ...)
    return self:get_bus():pcall(self.service_name, self.app_path, self.app_interface, method, signature, ...)
end

function Client:close()
    if self.bus then
        self.bus:close()
        self.bus = nil
    end
end

function Client:ping(bus, times, sleep_time)
    ping(bus, times, sleep_time, self.service_name, self.app_path)
end

return {Client = Client, BasicClient = BasicClient}